package ray;

import ray.math.Point3;
import ray.math.Vector3;

/**
 * Represents a simple camera.
 */
public class Camera {

    /*
     * Fields that are read in from the input file to describe the camera. You'll probably want to store some derived values to
     * make ray generation easy.
     */

    protected final Point3 viewPoint = new Point3();

    public void setViewPoint(Point3 viewPoint) {
        this.viewPoint.set(viewPoint);
    }

    protected final Vector3 viewDir = new Vector3(0, 0, -1);

    public void setViewDir(Vector3 viewDir) {
        this.viewDir.set(viewDir);
    }

    protected final Vector3 viewUp = new Vector3(0, 1, 0);

    public void setViewUp(Vector3 viewUp) {
        this.viewUp.set(viewUp);
    }

    protected final Vector3 projNormal = new Vector3(0, 0, 1);

    public void setProjNormal(Vector3 projNormal) {
        this.projNormal.set(projNormal);
    }

    protected double viewWidth = 1;

    public void setViewWidth(double viewWidth) {
        this.viewWidth = viewWidth;
    }

    protected double viewHeight = 1;

    public void setViewHeight(double viewHeight) {
        this.viewHeight = viewHeight;
    }

    protected double projDistance = 1;

    public void setprojDistance(double projDistance) {
        this.projDistance = projDistance;
    }

    /*
     * Derived values that are computed before ray generation. basisU, basisV, and basisW form an orthonormal basis. basisW is
     * parallel to projNormal.
     */
    protected final Vector3 basisU = new Vector3();
    protected final Vector3 basisV = new Vector3();
    protected final Vector3 basisW = new Vector3();
    protected final Vector3 centerDir = new Vector3();

    // Has the view been initialized?
    protected boolean initialized = false;

    /**
     * Initialize the derived view variables to prepare for using the camera.
     */
    public void initView() {
        projNormal.normalize();
        viewUp.normalize();
        
        // this could slightly change viewDir if getProjNormal() is viewDir,
        // and there are rounding errors
        viewDir.normalize();

        basisW.set(projNormal);

        // basisW should be pointing at the viewer
        if (basisW.dot(viewDir) >= 0) {
            basisW.scale(-1);
        }

        // this may not be necessary
        basisW.normalize();

        basisU.cross(viewUp, basisW);

        basisU.normalize();

        // if the image is reversed, try flipping this
        basisV.cross(basisW, basisU);

        basisV.normalize();

        initialized = true;
    }

    /**
     * Set outRay to be a ray from the camera through a point in the image.
     * 
     * @param outRay
     *            The output ray (not normalized)
     * @param inU
     *            The u coord of the image point (range [0,1])
     * @param inV
     *            The v coord of the image point (range [0,1])
     */
    public void getRay(Ray outRay, double inU, double inV) {
        if (!initialized) {
            initView();
        }

        Point3 centerOfImage = new Point3(viewPoint);
        centerOfImage.scaleAdd(projDistance, viewDir);

        double leftBound = -(viewWidth / 2);
        double bottomBound = -(viewHeight / 2);

        double rightBound = viewWidth / 2;
        double topBound = viewHeight / 2;

        double u = leftBound + (rightBound - leftBound) * inU;
        double v = bottomBound + (topBound - bottomBound) * inV;

        Point3 endPoint = new Point3(); // corresponds to `s` in the text
        endPoint.set(centerOfImage);

        endPoint.scaleAdd(u, basisU);
        endPoint.scaleAdd(v, basisV);

        Vector3 direction = new Vector3();
        direction.sub(endPoint, viewPoint);
        direction.normalize();

        outRay.set(viewPoint, direction);

        outRay.makeOffsetRay();
    }
}
